/*----------------------------------------------------------
* FILE: HibernateDaoFinderImpl.java
* LOGICAL NAME: HibernateDaoFinderImpl
* PRODUCT: bila-framework
*----------------------------------------------------------
* MODIFICATIONS
* DATE Sep 16, 2011
* AUTHOR wjuarez
*----------------------------------------------------------
*/

package bila.framework.dao.hibernate.impl;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Calendar;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.HibernateException;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Example;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.hibernate.jdbc.Work;
import org.hibernate.metadata.ClassMetadata;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

import bila.framework.commons.BeanHelper;
import bila.framework.commons.Constants;
import bila.framework.commons.GeneralFunction;
import bila.framework.dao.hibernate.HibernateDaoFinder;

/**
 * @author WJuarez
 * <b>Created by </b>wjuarez
 * <b>for</b> bila-framework
 * <b>package</b> bila.framework.dao.hibernate.impl
 * <b>on</b> Sep 16, 2011 9:23:38 PM
 * <b>Purpose</b> 
 * <p>
 *		Implementacion de DaoFinder utilizando framework Hibernate
 * </p>
 */
public class HibernateDaoFinderImpl<Model, ModelId extends Serializable> extends HibernateDaoSupport implements HibernateDaoFinder<Model, ModelId> {

	private Class<Model> clazz;
	private Log log = LogFactory.getLog(HibernateDaoFinderImpl.class);
	
	public HibernateDaoFinderImpl(Class<Model> clazz){
		this.clazz = clazz;
	}
	
	/*
	 * (non-Javadoc)
	 * @see bila.framework.dao.DaoFinder#find(java.lang.Object)
	 */
	public List<Model> find(Model model) throws Exception{
		DetachedCriteria criteria = createCriteriaExtend(model);
		return this.findByCriteria(criteria);
	}
	
	private DetachedCriteria createCriteriaExtend(Model model) throws Exception{
		DetachedCriteria criteria = DetachedCriteria.forClass(clazz);
		String name = "";
		Method method = null;
		Method[] methods = null;
		
		Object id = this.getObjectId(model);
		if(id != null){
			//Recupero los atributos de la llave
			methods = id.getClass().getMethods();
			//Recorro cada uno de ellos
			for(int i=0; i < methods.length; i++){
				method = methods[i];
				if(BeanHelper.isGetter(method.getName()) && !method.getName().equals(BeanHelper.GETCLASS)){
					name = method.getName().substring(3, method.getName().length());
					name = GeneralFunction.firstLower(name);
					if(!GeneralFunction.isNullOrEmptyOrZero(method.invoke(id, null)))
						criteria.add(Restrictions.eq("id." + name, method.invoke(id, null)));
				}
			}
		}
		
		//Recupero los atributos que no son de la llave
		methods = model.getClass().getMethods();
		for(int i=0; i < methods.length; i++){
			method = methods[i];
			if(BeanHelper.isGetter(method.getName()) && !method.getName().equals(BeanHelper.GETCLASS)){
				name = method.getName().substring(3, method.getName().length());
				name = GeneralFunction.firstLower(name);
				if(!name.equals("id") && !GeneralFunction.isNullOrEmptyOrZero(method.invoke(model, null)))
					criteria.add(Restrictions.eq(name, method.invoke(model, null)));
			}
		}
		return criteria;
	}

	/*
	 * (non-Javadoc)
	 * @see bila.framework.dao.DaoFinder#findByKey(java.lang.Object)
	 */
	public Model findByKey(ModelId model) throws Exception{
		Model modelRecover = getHibernateTemplate().get(clazz, model);
		return modelRecover;
	}

	/*
	 * (non-Javadoc)
	 * @see bila.framework.dao.DaoFinder#getAll()
	 */
	public List<Model> getAll() throws Exception{
		DetachedCriteria criteria = DetachedCriteria.forClass(clazz);
		return this.findByCriteria(criteria);
	}

	/*
	 * (non-Javadoc)
	 * @see bila.framework.dao.DaoFinder#findWithOrderBy(java.lang.Object, java.lang.String[], java.lang.String[])
	 */
	public List<Model> findWithOrderBy(Model model, String[] orders, String[] orderType) throws Exception{
		DetachedCriteria criteria = createCriteriaExtend(model);
		for (int i = 0; i < orders.length; i++)
			if(orderType[i].equals(Constants.ASC))
				criteria.addOrder(Order.asc(orders[i]));
			else if(orderType[i].equals(Constants.DESC))
				criteria.addOrder(Order.desc(orders[i]));
		return this.findByCriteria(criteria);
	}

	/*
	 * (non-Javadoc)
	 * @see bila.framework.dao.DaoFinder#getAllWithOrderBy(java.lang.String[], java.lang.String[])
	 */
	public List<Model> getAllWithOrderBy(String[] orders, String[] orderType) throws Exception{
		DetachedCriteria criteria = DetachedCriteria.forClass(clazz);
		for (int i = 0; i < orders.length; i++)
			if(orderType[i].equals(Constants.ASC))
				criteria.addOrder(Order.asc(orders[i]));
			else if(orderType[i].equals(Constants.DESC))
				criteria.addOrder(Order.desc(orders[i]));
		return this.findByCriteria(criteria);
	}

	/*
	 * (non-Javadoc)
	 * @see bila.framework.dao.hibernate.HibernateDaoFinder#sinchronyze()
	 */
	public void sinchronyze() throws Exception{
		getSession().flush();
	}

	/*
	 * (non-Javadoc)
	 * @see bila.framework.dao.hibernate.HibernateDaoFinder#findByHql(java.lang.String, java.lang.Object[])
	 */
	@SuppressWarnings("unchecked")
	public List<Model> findByHql(String hql, Object[] params) throws Exception{
		List<Model> list = getHibernateTemplate().find(hql, params);
		return list;
	}

	/*
	 * (non-Javadoc)
	 * @see bila.framework.dao.hibernate.HibernateDaoFinder#findByCriteria(org.hibernate.criterion.DetachedCriteria)
	 */
	@SuppressWarnings("unchecked")
	public List<Model> findByCriteria(DetachedCriteria criteria) throws Exception{
		List<Model> list = getHibernateTemplate().findByCriteria(criteria);
		return list;
	}

	/*
	 * (non-Javadoc)
	 * @see bila.framework.dao.DaoFinder#getByExample(java.lang.Object)
	 */
	public List<Model> getByExample(Model model) throws Exception{
		Example example = Example.create(model);
		example.
			ignoreCase().
				excludeZeroes().
					enableLike();
		DetachedCriteria criteria = DetachedCriteria.forClass(clazz);
		criteria.add(example);
		return this.findByCriteria(criteria);
	}

	/*
	 * (non-Javadoc)
	 * @see bila.framework.dao.DaoFinder#executeProcedure(java.lang.String, java.lang.Object[])
	 */
	public void executeProcedure(String sql, Object[] params) throws Exception{
		final String sqlToWork = sql;
		final Object[] paramsToWork = params;

		getSession().doWork(new Work() {
			public void execute(Connection conn) {
				CallableStatement cstmt = null;

				try {
					cstmt = conn.prepareCall(sqlToWork);
					if (paramsToWork != null) {
						for (int i = 0; i < paramsToWork.length; i++) {
							Object param = paramsToWork[i];
							log.debug("parametro -->" + param + " tipo-->"
									+ resolveType(param));
							cstmt.setObject(i + 1, param, resolveType(param));
						}
					}
					log.debug("begin executing ...>" + sqlToWork + "<");
					cstmt.execute();
					log.debug("end executing ...>" + sqlToWork + "<");
					cstmt.close();
				} catch (SQLException e) {
					try {
						cstmt.close();
					} catch (SQLException ex) {
						log.error("Error cerrando statement ...>"
								+ ex.getMessage() + "<");
						throw new HibernateException(e);
					}

					throw new HibernateException(e);

				} finally {
					cstmt = null;

				}

			}
		});
	}

	/*
	 * (non-Javadoc)
	 * @see bila.framework.dao.DaoFinder#executeFunction(java.lang.String, java.lang.Object[])
	 */
	public Object executeFunction(String sql, Object[] params) throws Exception{
		final Object[] outPutFinal = new Object[1];
		final String sqlFinal = sql;
		final Object[] paramsFinal = params;

		getSession().doWork(new Work() {

			public void execute(Connection conn) {

				CallableStatement cstmt = null;

				try {
					cstmt = conn.prepareCall(sqlFinal);

					if (paramsFinal != null) {
						for (int i = 0; i < paramsFinal.length; i++) {
							Object param = paramsFinal[i];
							if (i == 0) {
								cstmt.registerOutParameter(1,
										resolveType(param));
							} else {
								log.debug("parametro -->" + param + " tipo-->"
										+ resolveType(param));
								cstmt.setObject(i + 1, param,
										resolveType(param));
							}
						}
					}
					log.debug("begin executing ...>" + sqlFinal + "<");
					cstmt.execute();
					log.debug("end executing ...>" + sqlFinal + "<");
					Object obj = cstmt.getObject(1);
					outPutFinal[0] = obj;
					cstmt.close();
				} catch (SQLException e) {
					try {
						cstmt.close();
					} catch (SQLException ex) {
						log.error("Error cerrando statement ...>"
								+ ex.getMessage() + "<");
						throw new HibernateException(e);
					}

					throw new HibernateException(e);

				} finally {
					cstmt = null;

				}

			}
		});
		return outPutFinal[0];

	}

	/**
	 * Define cual es el tipo del parametro, estos tipos estan
	 * relacionados a los equivalentes de Java con las bases de datos
	 * @param param objeto que se desea conocer su tipo
	 * @return entero con el valor correspondiente al tipo
	 */
	private int resolveType(Object param) {
		int sqlType = Types.VARCHAR;
		if (param == null) {
			sqlType = Types.NULL;
		} else {
			Class<? extends Object> paramClass = param.getClass();
			if (param instanceof String) {
				sqlType = Types.VARCHAR;
			} else if (paramClass.equals(double.class)
					|| param instanceof Double) {
				sqlType = Types.DOUBLE;
			} else if (param instanceof BigDecimal) {
				sqlType = Types.NUMERIC;
			} else if (param instanceof Calendar
					|| param instanceof java.sql.Date) {
				sqlType = Types.DATE;
			} else if (paramClass.equals(int.class) || param instanceof Integer) {
				sqlType = Types.INTEGER;
			} else if (paramClass.equals(long.class) || param instanceof Long) {
				sqlType = Types.BIGINT;
			} else if (paramClass.equals(float.class) || param instanceof Float) {
				sqlType = Types.REAL;
			}
		}
		return sqlType;
	}
	
	/**
	 * Recupera el id o la llave primaria del objeto modelo
	 * Este objeto segun la definicion de Hibernate debe ser un objeto
	 * serializable
	 * @param obj de tipo Model del cual se desea conocer su Id
	 * @return objeto de tipo ModelId el cual debe tener implementado el Serializable
	 */
	private Serializable getObjectId(Object obj) throws Exception{
		String idProp = getMetadata().getIdentifierPropertyName();
		log.debug(" id for this class in hibernate config >" + idProp + "<");
		Object id = null;
		try {
			char[] chars = idProp.toUpperCase().toCharArray();
			String methodName = BeanHelper.GET + chars[0] + idProp.substring(1, idProp.length());
			log.debug(" id getter method for this class -->'" + methodName + "'");
			Method method = obj.getClass().getMethod(methodName, (Class[]) null);
			id = method.invoke(obj, (Object[]) null);
		} catch (Exception e) {
			log.info("infering that id class is a primitive type or a Lang primitive wrapper" + obj, e);
			return (Serializable) obj;
		}
		return (Serializable) id;
	}
	
	/**
	 * Recupera el metadata de la configuracion de Hibernate
	 * @return ClassMetadata segun la configuracion en los xml
	 */
	private ClassMetadata getMetadata() throws Exception{
		return getHibernateTemplate().
					getSessionFactory().
						getClassMetadata(clazz);
	}

	/*
	 * (non-Javadoc)
	 * @see bila.framework.dao.hibernate.HibernateDaoFinder#getConnection()
	 */
	@SuppressWarnings("deprecation")
	public Connection getConnection() throws Exception{
		Connection con = getHibernateTemplate().
				getSessionFactory().
				getCurrentSession().
				connection();
		return con;
	}

	/*
	 * (non-Javadoc)
	 * @see bila.framework.dao.hibernate.HibernateDaoFinder#getId(java.lang.String, java.lang.Object[])
	 */
	@SuppressWarnings("unchecked")
	@Override
	public Object getId(String hql, Object[] params) throws Exception {
		List<Object> list = getHibernateTemplate().find(hql, params);
		if (list.get(0) != null)
			return list.get(0);
		return null;
	}
	
}
